Skip to content

fix(Android): RTL layout for center-aligned custom header title#3896

Open
HakimMohamed wants to merge 3 commits intosoftware-mansion:mainfrom
HakimMohamed:fix/android-rtl-center-header-title
Open

fix(Android): RTL layout for center-aligned custom header title#3896
HakimMohamed wants to merge 3 commits intosoftware-mansion:mainfrom
HakimMohamed:fix/android-rtl-center-header-title

Conversation

@HakimMohamed
Copy link
Copy Markdown

Description

When headerTitleAlign is 'center' and headerTitle is a function (custom React component), the header title is invisible on Android in RTL mode. LTR works fine, iOS works fine.

Root cause: onNativeToolbarLayout sends paddingStart/paddingEnd (logical, direction-aware) to the C++ shadow tree, which uses them as physical left/right Yoga padding. In RTL, start=right and end=left, so the paddings were swapped — a large right-side value ended up as left padding, leaving near-zero width for the centered title.

Fixes #3438

Changes

  • Detect RTL via toolbar.layoutDirection in onNativeToolbarLayout
  • Convert logical start/end insets to physical left/right before sending to the shadow tree
  • Added Test3438.tsx reproducing the issue

Test plan

  • Open Test3438 in FabricExample
  • Set device/emulator language to an RTL language (e.g. Arabic)
  • Verify "Custom Title" is visible and centered in the header
  • Switch language back to English, verify LTR still works

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

The shadow tree expects physical left/right padding, but the toolbar
APIs return logical start/end values which swap sides in RTL. This
caused the center-aligned custom header title (headerTitle as function)
to receive near-zero width in RTL, making it invisible.

Fixes software-mansion#3438
Copy link
Copy Markdown
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the PR!

I'm not convinced this is the right fix yet.

Yoga is RTL aware & it should swap the paddings on its own, when the layout direction changes. I'll try to investigate it a bit deeper soon.

val isRtl = toolbar.layoutDirection == LAYOUT_DIRECTION_RTL

val contentInsetStartEstimation =
val startInsetEstimation =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary renaming. This value name should stay untouched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also applies to other names.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point dug into it and Yoga does handle logical edges correctly, but the existing setPadding() call maps paddingStart/paddingEnd to physical Edge::Left/Edge::Right, so the values never reach Yoga as logical. Pushed a commit that routes them through Edge::Start/Edge::End instead; tested in FabricExample and my production app, renders correctly in LTR and RTL.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an Android RTL rendering issue where a center-aligned custom headerTitle (function component) could become invisible due to start/end insets being interpreted as physical left/right padding in the Fabric shadow tree.

Changes:

  • Adjust Android header config layout reporting to account for RTL by mapping logical start/end insets to physical left/right padding.
  • Add a new issue-test (Test3438) and export it from the issue-tests index.
  • Update header layout inset calculations used to size/position header subviews in Fabric.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
apps/src/tests/issue-tests/index.ts Exports the new manual reproduction test for issue #3438.
apps/src/tests/issue-tests/Test3438.tsx Adds a manual reproduction case for centered custom header title in RTL.
android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt Updates inset-to-padding mapping to avoid RTL start/end swap issues when sending state to the Fabric shadow tree.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

} else {
paddingLeft = endInset
paddingRight =
if (leftSubview != null) toolbar.width - leftSubview.left else startInsetEstimation
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In RTL, toolbar.width - leftSubview.left overestimates the physical right padding because it includes the left-subview’s width (it measures from the subview’s left edge, not its right edge). This can still collapse the available width for the centered title. Compute the right-side inset from the toolbar’s right edge to the subview’s right edge instead (or otherwise subtract the subview width).

Suggested change
if (leftSubview != null) toolbar.width - leftSubview.left else startInsetEstimation
if (leftSubview != null) toolbar.width - leftSubview.right else startInsetEstimation

Copilot uses AI. Check for mistakes.
Comment on lines 140 to 142
val isBackButtonDisplayed = toolbar.navigationIcon != null
val isRtl = toolbar.layoutDirection == LAYOUT_DIRECTION_RTL

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LAYOUT_DIRECTION_RTL / LAYOUT_DIRECTION_LTR are used unqualified in this file, but there is no import for these constants. This won’t compile unless they’re referenced via View.LAYOUT_DIRECTION_* or imported explicitly.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +38
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerTitleAlign: 'center',
headerTitle: () => (
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>
Custom Title
</Text>
),
}}
/>
</Stack.Navigator>
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reproduction screen is the root of the stack, so Android typically won’t show a back button/navigation icon. If the original issue depends on the start inset created by the back button, this example may not trigger the bug. Consider adding a second screen and navigating to it so the header renders with a back button (and/or add a headerRight) to reliably reproduce #3438 in RTL.

Copilot uses AI. Check for mistakes.
Previously paddingStart/paddingEnd were sent to setPadding() which
maps to physical Edge::Left/Right — so the values were treated as
physical, not logical, and Yoga never swapped them in RTL.

Introduce setLogicalPadding() on the shadow node that sets padding
on Edge::Start/End, letting Yoga handle RTL direction on its own.
The Kotlin side now only converts leftSubview.left (physical) to
a logical start-side distance in RTL; the rest is physical-free.
@HakimMohamed HakimMohamed requested a review from kkafar April 17, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Missing title and an extra space appear when using RTL

3 participants